Skip to content
On this page

实战篇 9-小程序订单支付 —— 微信支付


实战篇 9:小程序订单支付 —— 小程序支付

创建完订单,这一节,我们来实现小程序的支付功能,以完成一个商业应用的业务经营能力闭环。

微信小程序的主要交互图如下:

要想实现支付的系统逻辑,最主要的是完成接下来的 4 个步骤:

1.小程序内调用登录接口

小程序内调用登录接口,获取到用户的 openid,API 参见 小程序登录 API

在面向小程序的 JWT 登录用户验证章节中,我们已经掌握了如何获取用户 openid 的调用流程与方法。

2. 商户 server 调用支付统一下单

获取了用户的 openid 后,需要在商户的 server 调用微信的支付统一下单,以创建一条待支付的记录返回给小程序,以完成小程序客户端的支付能力唤起,进入到后续步骤。商户 server 调用支付统一下单,API 参见 统一下单 API

看过接口文档,我们发现微信接收的数据与返回的格式都是以 text/xml 的格式,而非 application/json ,所以,我们需要引入 xml2js 的插件帮助我们在 JavaScript 的 Ojbect 与 XML 的 Object 数据关系之间快速转换。

$ npm i xml2js
// routers/order.js

{
  method: 'POST',
  path: `/${GROUP_NAME}/{orderId}/pay`,
  handler: async (request, reply) => {
    // 从用户表中获取 openid
    const user = await models.users.findOne({ where: { id: request.auth.credentials.userId } });
      const { openid } = user;
    // 构造 unifiedorder 所需入参
    const unifiedorderObj = {
      appid: config.wxAppid, // 小程序 id
      body: '小程序支付', // 商品简单描述
      mch_id: config.wxMchid, // 商户号
      nonce_str: Math.random().toString(36).substr(2, 15), // 随机字符串
      notify_url: 'https://yourhost.com/orders/pay/notify', // 支付成功的回调地址
      openid, // 用户 openid
      out_trade_no: request.params.orderId, // 商户订单号
      spbill_create_ip: request.info.remoteAddress, // 调用支付接口的用户 ip
      total_fee: 1, // 总金额,单位为分
      trade_type: 'JSAPI', // 交易类型,默认
    };
    // 签名的数据
    const getSignData = (rawData, apiKey) => {
      let keys = Object.keys(rawData);
      keys = keys.sort();
      let string = '';
      keys.forEach((key) => {
        string += `&${key}=${rawData[key]}`;
      });
      string = string.substr(1);
      return crypto.createHash('md5').update(`${string}&key=${apiKey}`).digest('hex').toUpperCase();
    };
    // 将基础数据信息 sign 签名
    const sign = getSignData(unifiedorderObj, config.wxPayApiKey);
    // 需要被 post 的数据源
    const unifiedorderWithSign = {
      ...unifiedorderObj,
      sign,
    };
    // 将需要 post 出去的订单参数,转换位 xml 格式
    const builder = new xml2js.Builder({ rootName: 'xml', headless: true });
    const unifiedorderXML = builder.buildObject(unifiedorderWithSign);
    const result = await axios({
      url: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
      method: 'POST',
      data: unifiedorderXML,
      headers: { 'content-type': 'text/xml' },
    });
    // result 是一个 xml 结构的 response,转换为 jsonObject,并返回前端
    xml2js.parseString(result.data, (err, parsedResult) => {
      if (parsedResult.xml) {
        if (parsedResult.xml.return_code[0] === 'SUCCESS'
        && parsedResult.xml.result_code[0] === 'SUCCESS') {
          // 待签名的原始支付数据
          const replyData = {
            appId: parsedResult.xml.appid[0],
            timeStamp: (Date.now() / 1000).toString(),
            nonceStr: parsedResult.xml.nonce_str[0],
            package: `prepay_id=${parsedResult.xml.prepay_id[0]}`,
            signType: 'MD5',
          };
          replyData.paySign = getSignData(replyData, config.wxPayApiKey);
          reply(replyData);
        }
      }
    });
  },
  config: {
    tags: ['api', GROUP_NAME],
    description: '支付某条订单',
    validate: {
      params: {
        orderId: Joi.string().required(),
      },
      ...jwtHeaderDefine,
    },
  },
},


3. 商户 server 调用再次签名

商户 server 调用再次签名,公共 API 参见 再次签名 API

此步骤由微信小程序前端客户端实现,将步骤 2 中,/orders/{orderId}/pay 接口返回的支付统一下单签名数据,依次填入 wx.requestPayment,即可在小程序的客户端唤起支付界面,并完成后续的支付操作流程。用户支付成功后,微信平台会自动触发步骤 4 中的支付成功推送。

wx.requestPayment(
{
  'timeStamp': '',
  'nonceStr': '',
  'package': '',
  'signType': 'MD5',
  'paySign': '',
  'success':function(res){},
  'fail':function(res){},
  'complete':function(res){}
})

4. 商户 server 接收支付通知

用户完成支付行为后,商户 server 接收支付通知,API 参见 支付结果通知 API

微信对商户后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但不保证通知最终能成功。(通知频率为 15/15/30/180/1800/1800/1800/1800/3600,单位:秒。)

商户应答成功的返回数据结构是:

<xml>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <return_msg><![CDATA[OK]]></return_msg>
</xml>

实现接口 POST /orders/pay/notify,在核对订单信息校验成功后,需要返回微信上述的 XML 字符串信息,否则返回 return_code 为 FAIL,并在 return_msg 中附带参数校验错误说明。/orders/pay/notify 在 hapi 的 API 接口中 config.auth 应该设置为 false,不进入 JWT 的用户认证流程。


{
  method: 'POST',
  path: `/${GROUP_NAME}/pay/notify`,
  handler: async (request, reply) => {
    xml2js.parseString(request.payload, async (err, parsedResult) => {
      if (parsedResult.xml.return_code[0] === 'SUCCESS') {
        // 微信统一支付状态成功,需要检验本地数据的逻辑一致性
        // 省略...细节逻辑校验
        // 更新该订单编号下的支付状态未已支付
        const orderId = parsedResult.xml.out_trade_no[0];
        const orderResult = await models.orders.findOne({ where: { id: orderId } });
        orderResult.payment_status = '1';
        await orderResult.save();
        // 返回微信,校验成功
        const retVal = {
          return_code: 'SUCCESS',
          return_msg: 'OK',
        };
        const builder = new xml2js.Builder({
          rootName: 'xml',
          headless: true,
        });
        reply(builder.buildObject(retVal));
      }
    });
  },
  config: {
    tags: ['api', GROUP_NAME],
    description: '微信支付成功的消息推送',
    auth: false,
  },
},


GitHub 参考代码 chapter13/hapi-tutorial-1

小结

关键词:微信支付,支付统一下单,支付通知,XML 数据通信

本小节围绕微信支付接入的四步骤,做了接入流程上的讲解。开发过程中尤其注意支付接入以 XML 格式数据进行数据交换,签名数据的算法一致性。剩下的对照着文档,小心翼翼地处理好字段的对应,便能顺利把流程走完。

本小节参考代码汇总

小程序内调用登录接口:小程序登录 API

商户 server 调用支付统一下单:统一下单 API

商户 server 调用再次签名:再次签名 API

商户 server 接收支付通知:支付结果通知 API

GitHub参考代码:chapter13/hapi-tutorial-1